﻿// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. 
// See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using System.Text;
using System.Threading.Tasks;
using Jurassic;
using Jurassic.Library;

namespace System.Threading.Actors.Languages
{
    public class JavascriptLanguagePackage : ILanguagePackage
    {
        private readonly string m_methodKeyPrefix = "$$js:";
        private readonly ScriptEngine m_jsEngine;
        private readonly StateInstance m_jsShadowState;
        private readonly IActorState m_actorState;
        private readonly PromiseConstructor m_promiseConstructor;
        private readonly ActorEventConstructor m_eventConstructor;

        public JavascriptLanguagePackage(IActorState state)
        {
            // Create a "shadow" javascript state provider
            m_actorState = state;
            m_jsEngine = new ScriptEngine();
            m_jsShadowState = new StateConstructor(m_jsEngine, state).Construct();
            m_promiseConstructor = new PromiseConstructor(m_jsEngine);
            m_eventConstructor = new ActorEventConstructor(m_jsEngine);
        }

        // Used during CallMethod to determine whether this language pack owns the method
        public bool OwnsMethod(string methodName)
        {
            object unused;
            return m_actorState.TryGet(m_methodKeyPrefix + methodName, out unused);
        }

        // Used during replication to determine whether this method needs to be added on
        // the replica.
        public bool OwnsKey(string methodKey)
        {
            return methodKey.StartsWith(m_methodKeyPrefix);
        }

        public void AddMethod(string methodName, string methodImpl)
        {
            m_jsEngine.Evaluate(methodImpl);
            m_actorState.Set(m_methodKeyPrefix + methodName, methodImpl);
        }

        public void AddMethod(string methodImpl)
        {
            m_jsEngine.Evaluate(methodImpl);
        }

        public object CallMethod(string methodName, params object[] methodArgs)
        {
            object tempObject;
            if (m_actorState.TryGet(m_methodKeyPrefix + methodName, out tempObject))
            {
                // This is a javascript method.
                var args = new object[methodArgs.Length + 1];
                args[0] = m_jsShadowState;
                for (int i = 0; i < methodArgs.Length; i++)
                {
                    // Need to intercept IActorPromise args and substitute something digestible by our script engine
                    var arg = methodArgs[i];
                    if (arg is IActorPromise) args[i + 1] = m_promiseConstructor.Construct(arg);
                    else if (arg is ActorEvent) args[i + 1] = m_eventConstructor.Construct(arg);
                    else args[i + 1] = arg;
                }
                object rc = m_jsEngine.CallGlobalFunction(methodName, args);
                if (rc is ObjectInstance)
                {
                    return JavascriptLanguageConversion.ConvertFromJavascript((ObjectInstance)rc);
                }
                else
                    return rc;
            }
            else throw new InvalidOperationException("Attempted to call non-existent JS method " + methodName);
        }
    }

    internal static class JavascriptLanguageConversion
    {
        private static JsonSerializerSettings s_noEmbeddedTypes = new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.None };

        // convert Javascript object to CLR object if possible
        // otherwise return same object (may already be CLR object)
        public static object ConvertFromJavascript(ObjectInstance jval)
        {
            StringInstance sval;
            NumberInstance nval;
            BooleanInstance bval;
            DateInstance dval;
            ArrayInstance aval;

            if ((sval = jval as StringInstance) != null)
            {
                return ConvertFromString(sval);
            }
            else if ((nval = jval as NumberInstance) != null)
            {
                return ConvertFromNumber(nval);
            }
            else if ((bval = jval as BooleanInstance) != null)
            {
                return ConvertFromBoolean(bval);
            }
            else if ((dval = jval as DateInstance) != null)
            {
                return ConvertFromDate(dval);
            }
            else if ((aval = jval as ArrayInstance) != null)
            {
                return ConvertFromArray(aval);
            }
            else
            {
                return ConvertFromObject(jval);
            }
        }

        // Converting to JSON does not include types
        public static string ConvertToJson(object val)
        {
            return JsonConvert.SerializeObject(val, Formatting.None, s_noEmbeddedTypes);
        }

        // Convert from javascript helper functions

        public static object ConvertFromObject(ObjectInstance jval)
        {
            // We do not try to convert arbitrary object
            return jval;
        }

        public static string ConvertFromString(StringInstance jval)
        {
            return jval.Value;
        }

        public static double ConvertFromNumber(NumberInstance jval)
        {
            return jval.Value;
        }

        public static bool ConvertFromBoolean(BooleanInstance jval)
        {
            return jval.Value;
        }

        public static DateTime ConvertFromDate(DateInstance jval)
        {
            return jval.Value;
        }

        public static object[] ConvertFromArray(ArrayInstance jval)
        {
            if (jval.ElementValues != null)
                return jval.ElementValues.ToArray();
            else
                return null;
        }
    }

    //
    // Some Jurassic-specific support classes
    //

    // Jurassic requires a "constructor" and an "instance" for each class that you
    // wish to "project" into their world.

    internal class StateConstructor : ClrFunction
    {
        private IActorState m_state;
        private ScriptEngine m_engine;
        public StateConstructor(ScriptEngine engine, IActorState state)
            : base(engine.Function.InstancePrototype, "ActorState", new StateInstance(engine.Object.InstancePrototype))
        {
            m_state = state;
            m_engine = engine;
        }

        [JSConstructorFunction]
        public StateInstance Construct()
        {
            return new StateInstance(this.InstancePrototype, m_state, m_engine);
        }
    }

    internal class StateInstance : ObjectInstance
    {
        private IActorState m_state;
        private ScriptEngine m_engine;
        private volatile ProxyConstructor m_proxyConstructor = null;

        public StateInstance(ObjectInstance prototype)
            : this(prototype, null, null)
        {
        }

        public StateInstance(ObjectInstance prototype, IActorState state, ScriptEngine engine)
            : base(prototype)
        {
            this.PopulateFunctions();
            m_state = state;
            m_engine = engine;
        }

        [JSFunction(Name = "set")]
        public void Set(string key, ObjectInstance value)
        {
            m_state.Set(key, JavascriptLanguageConversion.ConvertFromJavascript(value));
        }

        [JSFunction(Name = "remove")]
        public void Remove(string key)
        {
            m_state.Remove(key);
        }

        [JSFunction(Name = "get")]
        public object Get(string key)
        {
            var rval = m_state.Get(key);
            return rval;
        }

        [JSFunction(Name = "valueOrDefault")]
        public object ValueOrDefault(string key, object defaultValue)
        {
            object result;
            if (!m_state.TryGet(key, out result))
                result = defaultValue;
            return result;
        }

        [JSFunction(Name = "contains")]
        public bool Contains(string key)
        {
            object temp;
            return m_state.TryGet(key, out temp);
        }

        [JSFunction(Name = "callMethod")]
        public object CallMethod(string methodName, params object[] args)
        {
            if (args != null)
            {
                for (int i = 0; i < args.Length; i++)
                {
                    if (args[i] is ObjectInstance)
                    {
                        args[i] = JavascriptLanguageConversion.ConvertFromJavascript((ObjectInstance)args[i]);
                    }
                }
            }

            object ret =  m_state.CallMethod(methodName, args);
            if (ret != null)
            {
                return JavascriptLanguageConversion.ConvertToJson(ret);
            }
            else
            {
                return null;
            }
        }

        [JSFunction(Name = "getActorProxy")]
        public ObjectInstance GetActorProxy(string otherActorName, string partitionName)
        {
            var proxy = m_state.GetActorProxy(otherActorName, partitionName);
            if (m_proxyConstructor == null)
            {
                lock (m_engine)
                {
                    if (m_proxyConstructor == null)
                    {
                        m_proxyConstructor = new ProxyConstructor(m_engine);
                    }
                }
            }
            var result = m_proxyConstructor.Construct(proxy);
            return result;
        }

        [JSFunction(Name = "createActor")]
        public bool CreateActor(string actorName, string creationString)
        {
            return m_state.CreateActor(actorName, creationString);
        }

        [JSFunction(Name = "deleteActor")]
        public void DeleteActor(string actorName)
        {
            m_state.DeleteActor(actorName);
        }

        // TODO: Test.
        [JSFunction(Name = "publish")]
        public void Publish(string eventType, object payload, params object[] payloadQualifiers)
        {
            m_state.Publish(eventType, payload, payloadQualifiers);
        }
    }

#region IActorProxy support
    internal class ProxyConstructor : ClrFunction
    {
        //private IActorProxy m_proxy;
        private ScriptEngine m_engine;
        public ProxyConstructor(ScriptEngine engine/*, IActorProxy proxy*/)
            : base(engine.Function.InstancePrototype, "ActorProxy", new ProxyInstance(engine.Object.InstancePrototype))
        {
            m_engine = engine;
        }

        [JSConstructorFunction]
        public ProxyInstance Construct(object proxyObject /*IActorProxy proxy*/)
        {
            return new ProxyInstance(this.InstancePrototype, (IActorProxy)proxyObject, m_engine);
        }
    }

    internal class ProxyInstance : ObjectInstance
    {
        private IActorProxy m_proxy;
        private volatile PromiseConstructor m_promiseConstructor = null;
        private volatile ObservableConstructor m_observableConstructor = null;
        private ScriptEngine m_engine;

        public ProxyInstance(ObjectInstance prototype)
            : this(prototype, null, null)
        {
        }

        public ProxyInstance(ObjectInstance prototype, IActorProxy proxy, ScriptEngine engine)
            : base(prototype)
        {
            this.PopulateFunctions();
            m_proxy = proxy;
            m_engine = engine;
        }

        [JSFunction(Name = "command")]
        public void Command(string methodName, params object[] args)
        {
            m_proxy.Command(methodName, args);
        }

        [JSFunction(Name = "request")]
        public ObjectInstance Request(string methodName, params object[] args)
        {
            var promise = m_proxy.Request(methodName, args);
            if (m_promiseConstructor == null)
            {
                lock (m_engine)
                {
                    if (m_promiseConstructor == null)
                    {
                        m_promiseConstructor = new PromiseConstructor(m_engine);
                    }
                }
            }
            return m_promiseConstructor.Construct(promise);
        }

        [JSFunction(Name = "getObservable")]
        public ObjectInstance GetObservable(string eventType)
        {
            var observable = m_proxy[eventType];
            if (m_observableConstructor == null)
            {
                lock (m_engine)
                {
                    if (m_observableConstructor == null)
                    {
                        m_observableConstructor = new ObservableConstructor(m_engine);
                    }
                }
            }
            return m_observableConstructor.Construct(observable);
        }
    }

#endregion IActorProxy support

    #region IActorStateObservable support

    internal class ObservableConstructor : ClrFunction
    {
        private ScriptEngine m_engine;
        public ObservableConstructor(ScriptEngine engine)
            : base(engine.Function.InstancePrototype, "ActorObservable", new ObservableInstance(engine.Object.InstancePrototype))
        {
            m_engine = engine;
        }

        [JSConstructorFunction]
        public ObservableInstance Construct(object observableObject)
        {
            return new ObservableInstance(this.InstancePrototype, (IActorStateObservable)observableObject, m_engine);
        }
    }

    internal class ObservableInstance : ObjectInstance
    {
        private IActorStateObservable m_observable;
        private ScriptEngine m_engine;

        public ObservableInstance(ObjectInstance prototype)
            : this(prototype, null, null)
        {
        }

        public ObservableInstance(ObjectInstance prototype, IActorStateObservable observable, ScriptEngine engine)
            : base(prototype)
        {
            m_engine = Engine;
            this.PopulateFunctions();
            m_observable = observable;
        }

        [JSFunction(Name = "subscribe")]
        public void Subscribe(string eventHandlerName) // TODO: Return/Extend a disposable, so that unsubscribe-via-dispose works
        {
            m_observable.Subscribe(eventHandlerName);
        }

        [JSFunction(Name = "unsubscribe")]
        public void Unsubscribe(string eventHandlerName)
        {
            m_observable.Unsubscribe(eventHandlerName);
        }

    }

    #endregion IActorStateObservable support

    #region IActorPromise support
    internal class PromiseConstructor : ClrFunction
    {
        private ScriptEngine m_engine;
        public PromiseConstructor(ScriptEngine engine)
            : base(engine.Function.InstancePrototype, "ActorPromise", new PromiseInstance(engine.Object.InstancePrototype))
        {
            m_engine = engine;
        }

        [JSConstructorFunction]
        public PromiseInstance Construct(object promiseObject)
        {
            return new PromiseInstance(this.InstancePrototype, (IActorPromise)promiseObject, m_engine);
        }
    }

    internal class PromiseInstance : ObjectInstance
    {
        private IActorPromise m_promise;
        private ScriptEngine m_engine;

        public PromiseInstance(ObjectInstance prototype)
            : this(prototype, null, null)
        {
        }

        public PromiseInstance(ObjectInstance prototype, IActorPromise promise, ScriptEngine engine)
            : base(prototype)
        {
            m_engine = Engine;
            this.PopulateFunctions();
            this.PopulateFields();
            m_promise = promise;
        }

        // Returns JSON-ized result
        [JSFunction(Name = "getResult")]
        public string GetResult()
        {
            return m_promise.Result;
        }

        [JSFunction(Name = "getIsFaulted")]
        public bool GetIsFaulted()
        {
            return m_promise.IsFaulted;
        }

        [JSFunction(Name = "getExceptionType")]
        public string GetExceptionType()
        {
            return m_promise.ExceptionType;
        }

        [JSFunction(Name = "getExceptionMessage")]
        public string GetExceptionMessage()
        {
            return m_promise.ExceptionMessage;
        }

        [JSFunction(Name = "getExceptionStackTrace")]
        public string GetExceptionStackTrace()
        {
            return m_promise.ExceptionStackTrace;
        }

        [JSFunction(Name = "continueWith")]
        public PromiseInstance ContinueWith(string continuationMethodName)
        {
            IActorPromise continuationPromise = m_promise.ContinueWith(continuationMethodName);
            return new PromiseInstance(m_engine.Object.InstancePrototype, continuationPromise, m_engine);
        }

        [JSFunction(Name = "wait")]
        public void Wait()
        {
            m_promise.Wait();
        }

        // Hack for a unit test
        public IActorPromise Unwrap()
        {
            return m_promise;
        }
    }

    #endregion IActorPromise support


#region ActorEvent support
    internal class ActorEventConstructor : ClrFunction
    {
        private ScriptEngine m_engine;
        public ActorEventConstructor(ScriptEngine engine)
            : base(engine.Function.InstancePrototype, "ActorEvent", new ActorEventInstance(engine.Object.InstancePrototype))
        {
            m_engine = engine;
        }

        [JSConstructorFunction]
        public ActorEventInstance Construct(object eventObject)
        {
            return new ActorEventInstance(this.InstancePrototype, (ActorEvent)eventObject, m_engine);
        }
    }

    internal class ActorEventInstance : ObjectInstance
    {
        private ActorEvent m_event;
        private ScriptEngine m_engine;

        public ActorEventInstance(ObjectInstance prototype)
            : this(prototype, null, null)
        {
        }

        public ActorEventInstance(ObjectInstance prototype, ActorEvent ae, ScriptEngine engine)
            : base(prototype)
        {
            m_engine = engine;
            this.PopulateFunctions();
            m_event = ae;
        }

        [JSFunction(Name = "getSource")]
        public string GetEventSource()
        {
            return m_event.EventSource;
        }

        [JSFunction(Name = "getSourcePartitionName")]
        public string GetEventSourcePartitionName()
        {
            return m_event.EventSourcePartitionName;
        }

        [JSFunction(Name = "getType")]
        public string GetEventType()
        {
            return m_event.EventType;
        }

        [JSFunction(Name = "getPayload")]
        public string GetEventPayload()
        {
            return m_event.EventPayload;
        }

        [JSFunction(Name = "getPayloadQualifier")]
        public string GetEventPayloadQualifier(int index)
        {
            return m_event.EventPayloadQualifiers[index];
        }
    }

#endregion ActorEvent support
}
